Lær de grundlæggende koncepter og avancerede teknikker til real-tids skyggegengivelse i WebGL. Denne guide dækker shadow mapping, PCF, CSM og løsninger på almindelige artefakter.
WebGL Shadow Mapping: En omfattende guide til real-tids gengivelse
I en verden af 3D-computer grafik, er der få elementer, der bidrager mere til realisme og fordybelse end skygger. De giver afgørende visuelle signaler om de rumlige forhold mellem objekter, placeringen af lyskilder og den overordnede geometri i en scene. Uden skygger kan 3D-verdener føles flade, usammenhængende og kunstige. For webbaserede 3D-applikationer drevet af WebGL, er implementering af real-tidsskygger af høj kvalitet et kendetegn for professionelle oplevelser. Denne guide giver et dybtgående indblik i den mest grundlæggende og udbredte teknik til at opnå dette: Shadow Mapping.
Uanset om du er en erfaren grafikprogrammør eller en webudvikler, der begiver sig ind i den tredje dimension, vil denne artikel udstyre dig med viden til at forstå, implementere og fejlfinde real-tidsskygger i dine WebGL-projekter. Vi vil rejse fra kerneteorien til praktiske implementeringsdetaljer og udforske almindelige faldgruber og de avancerede teknikker, der bruges i moderne grafikmotorer.
Kapitel 1: Det grundlæggende i Shadow Mapping
I sin kerne er shadow mapping en smart og elegant teknik, der afgør, om et punkt i en scene er i skygge ved at stille et simpelt spørgsmål: "Kan dette punkt ses af lyskilden?" Hvis svaret er nej, betyder det, at noget blokerer lyset, og punktet må være i skygge. For at besvare dette spørgsmål programmatisk bruger vi en to-pass renderingstilgang.
Hvad er Shadow Mapping? Kerneprincippet
Hele teknikken drejer sig om at rendere scenen to gange, hver gang fra et andet synspunkt:
- Pass 1: Dybdepasset (lysets perspektiv). Først renderer vi hele scenen fra lyskildens nøjagtige position og orientering. Vi er dog ligeglade med farver eller teksturer i dette pass. Den eneste information, vi har brug for, er dybde. For hvert objekt, der renderes, registrerer vi dets afstand fra lyskilden. Denne samling af dybdeværdier gemmes i en speciel tekstur kaldet et skyggemaskekort eller dybdekort. Hver pixel i dette kort repræsenterer afstanden til det nærmeste objekt fra lysets synspunkt i en bestemt retning.
- Pass 2: Sceneskiftet (kameraets perspektiv). Dernæst renderer vi scenen, som vi normalt ville gøre, fra hovedkameraets perspektiv. Men for hver eneste pixel, der tegnes, udfører vi en yderligere beregning. Vi bestemmer den pixels position i 3D-rummet og spørger derefter: "Hvor langt er dette punkt fra lyskilden?" Vi sammenligner derefter denne afstand med den værdi, der er gemt i vores skyggemaskekort (fra Pass 1) på den tilsvarende placering.
Logikken er enkel:
- Hvis pixelens nuværende afstand fra lyset er større end den afstand, der er gemt i skyggemaskekortet, betyder det, at der er et andet objekt tættere på lyset langs den samme synslinje. Derfor er den aktuelle pixel i skygge.
- Hvis pixelens afstand er mindre end eller lig med afstanden i skyggemaskekortet, betyder det, at intet blokerer den, og pixlen er fuldt oplyst.
Opsætning af scenen
For at implementere skyggekortlægning i WebGL har du brug for flere nøglekomponenter:
- En lyskilde: Dette kan være et retningsbestemt lys (som solen), et punktlys (som en pære) eller en spotlight. Lystypen bestemmer den type projektionsmatrix, der bruges under dybdepasset.
- Et Framebuffer-objekt (FBO): WebGL renderer normalt til skærmens standardframebuffer. For at oprette vores skyggemaskekort har vi brug for et off-screen render-target. En FBO giver os mulighed for at rendere til en tekstur i stedet for skærmen. Vores FBO vil blive konfigureret med en dybdeteksturtilknytning.
- To sæt shaders: Du skal bruge et shaderprogram til dybdepasset (et meget simpelt) og et andet til det endelige sceneskift (som vil indeholde skyggeberegningslogikken).
- Matricer: Du skal bruge standardmodel-, visnings- og projektionsmatricer til kameraet. Afgørende er, at du også skal bruge en visnings- og projektionsmatrix til lyskilden, ofte kombineret i en enkelt "lysrummatrix".
Kapitel 2: Two-Pass Rendering Pipeline i detaljer
Lad os nedbryde de to renderingspas trin for trin og fokusere på matricernes og shadernes roller.
Pass 1: Dybdepasset (fra lysets perspektiv)
Målet med dette pass er at udfylde vores dybdetekstur. Sådan fungerer det:- Bind FBO'en: Før du tegner, instruerer du WebGL til at rendere til din brugerdefinerede FBO i stedet for lærredet.
- Konfigurer Viewporten: Indstil viewport-dimensionerne, så de matcher størrelsen på din skyggemaskekorttekstur (f.eks. 1024x1024 pixels).
- Ryd dybdebufferen: Sørg for, at FBO'ens dybdebuffer ryddes før rendering.
- Opret lysets matricer:
- Lysvisningsmatrix: Denne matrix transformerer verden til lysets synspunkt. For et retningsbestemt lys oprettes dette typisk med en `lookAt`-funktion, hvor "øjet" er lysets position, og "målet" er den retning, det peger.
- Lystekstur Matrix: For et retningsbestemt lys, som har parallelle stråler, bruges en ortografisk projektion. For punktlys eller spotlights bruges en perspektiv projektion. Denne matrix definerer det rumfang i rummet (en kasse eller en frustum), der vil kaste skygger.
- Brug Dybde Shader-programmet: Dette er en minimal shader. Vertex shaderens eneste job er at gange vertex-positionen med lysets visnings- og projektionsmatricer. Fragment shaderen er endnu enklere: den skriver bare fragmentets dybdeværdi (dens z-koordinat) ind i dybdeteksturen. I moderne WebGL behøver du ofte ikke engang en brugerdefineret fragment shader, da FBO'en kan konfigureres til automatisk at fange dybdebufferen.
- Render Scenen: Tegn alle skyggekastende objekter i din scene. FBO'en indeholder nu vores færdige skyggemaskekort.
Pass 2: Sceneskiftet (fra kameraets perspektiv)
Nu renderer vi det endelige billede ved hjælp af det skyggemaskekort, vi lige har oprettet, til at bestemme skygger.
- Fjernbinding af FBO'en: Skift tilbage til rendering til standardlærredets framebuffer.
- Konfigurer Viewporten: Indstil viewporten tilbage til lærredets dimensioner.
- Ryd Skærmen: Ryd farve- og dybdebuffere på lærredet.
- Brug Scene Shader-programmet: Det er her magien sker. Denne shader er mere kompleks.
- Vertex Shader: Denne shader skal gøre to ting. For det første beregner den den endelige vertex-position ved hjælp af kameraets model-, visnings- og projektionsmatricer som normalt. For det andet skal den også beregne vertexens position fra lysets perspektiv ved hjælp af lysrummatricen fra Pass 1. Denne anden koordinat sendes til fragment shaderen som en varierende.
- Fragment Shader: Dette er kernen i skyggelogikken. For hvert fragment:
- Modtag den interpolerede position i lysrummet fra vertex shaderen.
- Udfør en perspektivdeling på denne koordinat (del x, y, z med w). Dette transformerer den til Normaliserede Enhedskoordinater (NDC), der spænder fra -1 til 1.
- Transformer NDC'en til teksturkoordinater (som spænder fra 0 til 1), så vi kan sample vores skyggemaskekort. Dette er en simpel skalering- og biasoperation: `texCoord = ndc * 0.5 + 0.5;`.
- Brug disse teksturkoordinater til at sample skyggemaskekortteksturen, der blev oprettet i Pass 1. Dette giver os `depthFromShadowMap`.
- Fragmentets nuværende dybde fra lysets perspektiv er dets z-komponent fra den transformerede lysrumskoordinat. Lad os kalde det `currentDepth`.
- Sammenlign dybderne: Hvis `currentDepth > depthFromShadowMap`, er fragmentet i skygge. Vi bliver nødt til at tilføje en lille bias til dette check for at undgå en artefakt kaldet "skyggemaskekort", som vi vil diskutere næste gang.
- Baseret på sammenligningen skal du bestemme en skyggefaktor (f.eks. 1.0 for oplyst, 0.3 for skyggelagt).
- Anvend denne skyggefaktor på den endelige farveberegning (f.eks. gang de omgivende og diffuse belysningskomponenter med skyggefaktoren).
- Render Scenen: Tegn alle objekter i scenen.
Kapitel 3: Almindelige problemer og løsninger
Implementering af grundlæggende skyggekortlægning vil hurtigt afsløre flere almindelige visuelle artefakter. Det er afgørende at forstå og rette dem for at opnå resultater af høj kvalitet.
Skyggeakne (selvskyggeartefakter)
Problemet: Du kan se mærkelige, forkerte mønstre af mørke linjer eller Moiré-lignende mønstre på overflader, der skal være fuldt oplyste. Dette kaldes "skyggeakne". Det opstår, fordi dybdeværdien, der er gemt i skyggemaskekortet, og den dybdeværdi, der er beregnet under sceneskiftet, er for den samme overflade. På grund af flydende punkt unøjagtigheder og den begrænsede opløsning af skyggemaskekortet, kan små fejl få et fragment til fejlagtigt at afgøre, at det er bag sig selv, hvilket resulterer i selvskygge.
Løsningen: Dybdebias. Den enkleste løsning er at introducere en lille bias til `currentDepth` før sammenligningen. Ved at få fragmentet til at se lidt tættere på lyset, end det faktisk er, skubber vi det "ud" af sin egen skygge.
float shadow = currentDepth > depthFromShadowMap + bias ? 0.3 : 1.0;
At finde den rigtige biasværdi er en delikat balancegang. For lille, og aknen forbliver. For stor, og du får det næste problem.
Peter Pan
Problemet: Denne artefakt, opkaldt efter karakteren, der kunne flyve og mistede sin skygge, manifesterer sig som et synligt hul mellem et objekt og dets skygge. Det får objekter til at se ud som om de flyder eller er afbrudt fra de overflader, de skal hvile på. Det er det direkte resultat af at bruge en dybdebias, der er for stor.
Løsningen: Hældningsskala Dybdebias. En mere robust løsning end en konstant bias er at gøre bias afhængig af overfladens stejlhed i forhold til lyset. Stejlere polygoner er mere tilbøjelige til akne og kræver en større bias. Fladere polygoner har brug for en mindre bias. De fleste grafik-API'er, herunder WebGL, giver funktionalitet til automatisk at anvende denne type bias under dybdepasset, hvilket generelt er at foretrække frem for en manuel bias i fragment shaderen.
Perspektiv Aliasing (takkede kanter)
Problemet: Kanterne af dine skygger ser blokerede, takkede og pixelerede ud. Dette er en form for aliasing. Det sker, fordi opløsningen af skyggemaskekortet er endelig. En enkelt pixel (eller texel) i skyggemaskekortet kan dække et stort område på en overflade i den endelige scene, især for overflader nær kameraet eller dem, der ses i en græsningsvinkel. Denne uoverensstemmelse i opløsning forårsager det karakteristiske blokerede udseende.
Løsningen: Forøgelse af skyggemaskekortopløsningen (f.eks. fra 1024x1024 til 4096x4096) kan hjælpe, men det kommer med en betydelig hukommelses- og ydelsesomkostning og løser ikke fuldt ud det underliggende problem. De rigtige løsninger ligger i mere avancerede teknikker.
Kapitel 4: Avancerede skyggekortlægningsteknikker
Grundlæggende skyggekortlægning giver et fundament, men professionelle applikationer bruger mere sofistikerede algoritmer til at overvinde dens begrænsninger, især aliasing.
Percentage-Closer Filtering (PCF)
PCF er den mest almindelige teknik til at blødgøre skyggekanter og reducere aliasing. I stedet for at tage en enkelt prøve fra skyggemaskekortet og træffe en binær beslutning (i skygge eller ikke-i-skygge), tager PCF flere prøver fra området omkring målkoordinaten.
Konceptet: For hvert fragment sampler vi skyggemaskekortet ikke kun én gang, men i et gittermønster (f.eks. 3x3 eller 5x5) omkring fragmentets projicerede teksturkoordinat. For hver af disse prøver udfører vi dybdesammenligningen. Den endelige skyggeværdi er gennemsnittet af alle disse sammenligninger. For eksempel, hvis 4 ud af 9 prøver er i skygge, vil fragmentet være 4/9-dele skyggelagt, hvilket resulterer i en glat penumbra (den bløde kant af en skygge).
Implementering: Dette gøres udelukkende inden for fragment shaderen. Det involverer en løkke, der itererer over en lille kerne, sampler skyggemaskekortet ved hver forskydning og akkumulerer resultaterne. WebGL 2 tilbyder hardwareunderstøttelse (`texture` med en `sampler2DShadow`), der kan udføre sammenligningen og filtreringen mere effektivt.
Fordel: Forbedrer skyggekvaliteten drastisk ved at erstatte hårde, aliasede kanter med glatte, bløde kanter.
Omkostninger: Ydelsen falder med antallet af prøver, der tages pr. fragment.
Cascaded Shadow Maps (CSM)
CSM er industristandardløsningen til gengivelse af skygger fra en enkelt retningsbestemt lyskilde (som solen) over en meget stor scene. Det tackler direkte problemet med perspektiv aliasing.
Konceptet: Kerneideen er, at objekter tæt på kameraet har brug for meget højere skyggeopløsning end objekter langt væk. CSM opdeler kameraets synsfrustum i flere sektioner eller "kaskader" langs dens dybde. Et separat skyggemaskekort af høj kvalitet gengives derefter for hver kaskade. Kaskaden tættest på kameraet dækker et lille område af verdensrummet og har således meget høj effektiv opløsning. Kaskader længere væk dækker gradvist større områder med den samme teksturstørrelse, hvilket er acceptabelt, fordi disse detaljer er mindre synlige for spilleren.
Implementering: Dette er væsentligt mere komplekst.
- I CPU'en skal du opdele kamerafrustrumet i 2-4 kaskader.
- For hver kaskade skal du beregne en tætsiddende ortografisk projektionsmatrix for lyset, der perfekt omslutter den sektion af frustrumet.
- I renderingsløkken skal du udføre dybdepasset flere gange - en gang for hver kaskade og gengive til et andet skyggemaskekort (eller et område af et teksturatlas).
- I fragment shaderen for den endelige scene skal du bestemme, hvilken kaskade det aktuelle fragment tilhører, baseret på dets afstand fra kameraet.
- Sample den relevante kaskades skyggemaskekort for at beregne skyggen.
Fordel: Giver konstant høje opløsningsskygger over store afstande, hvilket gør det perfekt til udendørs miljøer.
Variance Shadow Maps (VSM)
VSM er en anden teknik til at skabe bløde skygger, men den tager en anden tilgang end PCF.
Konceptet: I stedet for kun at gemme dybden i skyggemaskekortet, gemmer VSM to værdier: dybden (det første moment) og den kvadrerede dybde (det andet moment). Disse to værdier giver os mulighed for at beregne variansen af dybdefordelingen. Ved hjælp af et matematisk værktøj kaldet Chebyshev's ulighed kan vi derefter estimere sandsynligheden for, at et fragment er i skygge. Den vigtigste fordel er, at en VSM-tekstur kan sløres ved hjælp af standard hardwareaccelereret lineær filtrering og mipmapping, noget der er matematisk ugyldigt for et standarddybdekort. Dette giver mulighed for meget store, bløde og glatte skyggepenumbraer med en fast ydelsesomkostning.
Ulempe: VSM's største svaghed er "lysblødning", hvor lys kan se ud til at bløde gennem objekter i situationer med overlappende okkluderere, da den statistiske tilnærmelse kan bryde sammen.
Kapitel 5: Praktiske implementeringstips og ydeevne
Valg af din skyggemaskekortopløsning
Opløsningen af dit skyggemaskekort er en direkte afvejning mellem kvalitet og ydeevne. En større tekstur giver skarpere skygger, men bruger mere videohukommelse og tager længere tid at rendere og sample. Almindelige størrelser inkluderer:
- 1024x1024: En god baseline for mange applikationer.
- 2048x2048: Tilbyder en mærkbar kvalitetsforbedring til desktopapplikationer.
- 4096x4096: Høj kvalitet, ofte brugt til helteaktiver eller i motorer med robust beskæring.
Optimering af lysets Frustum
For at få mest muligt ud af hver pixel i dit skyggemaskekort er det afgørende, at lysets projektionsvolumen (dens ortografiske boks eller perspektivfrustum) er så tæt tilpasset som muligt til de sceneelementer, der har brug for skygger. For et retningsbestemt lys betyder det at tilpasse dets ortografiske projektion til kun at omslutte den synlige del af kameraets frustum. Ethvert spildt rum i skyggemaskekortet er spildt opløsning.
WebGL-udvidelser og -versioner
WebGL 1 vs. WebGL 2: Selvom skyggekortlægning er mulig i WebGL 1, er det meget lettere og mere effektivt i WebGL 2. WebGL 1 kræver udvidelsen `WEBGL_depth_texture` for at oprette en dybdetekstur. WebGL 2 har denne funktionalitet indbygget. Desuden giver WebGL 2 adgang til skyggesamplingsenheder (`sampler2DShadow`), som kan udføre hardwareaccelereret PCF, hvilket giver et betydeligt ydelsesboost i forhold til manuelle PCF-løkker i shaderen.
Fejlfinding af skygger
Skygger kan være notorisk vanskelige at fejlfinde. Den mest nyttige teknik er at visualisere skyggemaskekortet. Rediger midlertidigt din applikation for at gengive dybdeteksturen fra en bestemt lyskilde direkte på en quad på skærmen. Dette giver dig mulighed for at se præcis, hvad lyset "ser". Dette kan straks afsløre problemer med lysets matricer, frustum culling eller objektrendering under dybdepasset.
Konklusion
Real-tids skyggekortlægning er en hjørnesten i moderne 3D-grafik, der transformerer flade, livløse scener til troværdige og dynamiske verdener. Selvom konceptet med at gengive fra et lys' perspektiv er simpelt, kræver det at opnå resultater af høj kvalitet uden artefakter en dyb forståelse af den underliggende mekanik, fra to-pass-pipelinen til nuancerne i dybdebias og aliasing.
Ved at starte med en grundlæggende implementering kan du gradvist tackle almindelige artefakter som skyggeakne og takkede kanter. Derfra kan du løfte dine visuals med avancerede teknikker som PCF til bløde skygger eller Cascaded Shadow Maps til store miljøer. Rejsen ind i skyggerendering er et perfekt eksempel på blandingen af kunst og videnskab, der gør computergrafik så overbevisende. Vi opfordrer dig til at eksperimentere med disse teknikker, skubbe deres grænser og bringe et nyt niveau af realisme til dine WebGL-projekter.